<!doctype html>
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<html>
<head>
  <meta charset="utf-8">
  <script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
  <script src="wct-browser-config.js"></script>
  <script src="../../node_modules/wct-browser-legacy/browser.js"></script>
  <script type="module" src="../../polymer-element.js"></script>
  <script type="module" src="../../lib/mixins/gesture-event-listeners.js"></script>
  <script type="module" src="../../lib/elements/dom-if.js"></script>
<body>

<script type="module">
import { PolymerElement } from '../../polymer-element.js';
import '../../lib/mixins/gesture-event-listeners.js';
import '../../lib/elements/dom-if.js';
class XElementChild extends PolymerElement {
  ready() {
    super.ready();
    window.lifecycleOrder.log(this, 'ready');
  }
}
customElements.define('x-element-child', XElementChild);
</script>

<dom-module id="x-element">
  <template>
    [[prop]] - [[path]]
    <x-element-child id="noBinding" log></x-element>
    <x-element-child id="hasBinding" prop="{{prop}}" path="{{path}}" log></x-element>
    <x-element-child id="events" on-click="handleClick" on-tap="handleTap"></x-element-child>
  </template>
  <script type="module">
import { PolymerElement } from '../../polymer-element.js';
import '../../lib/mixins/gesture-event-listeners.js';
import '../../lib/elements/dom-if.js';
class XElement extends PolymerElement {
  static get is() { return 'x-element'; }
  static get observers() { return ['propChanged(prop)', 'pathChanged(path)']; }
  static get properties() { return { prop: { notify: true }, path: { notify: true } }; }
  constructor() {
    super();
    this.propChanged = sinon.spy();
    this.pathChanged = sinon.spy();
  }
  ready() {
    super.ready();
    this.readied = true;
    window.lifecycleOrder.log(this, 'ready');
  }
}
customElements.define('x-element', XElement);
</script>
</dom-module>

<dom-module id="x-runtime">
  <template>
    <!-- Main template content -->
    <style>
      x-element {
        display: block;
        border-bottom: 10px solid orange;
      }
    </style>
    <x-element id="first"></x-element>
    <x-element id="textBinding">[[prop]] - [[obj.path]] - [[compute(prop, obj.path)]]</x-element>
    <x-element id="propBinding" prop="{{prop}}" log="proto"></x-element>
    <x-element id="pathBinding" path="{{obj.path}}"></x-element>
    <x-element id="compoundPropBinding" compound="[[prop]] - [[obj.path]] - [[compute(prop, obj.path)]]"></x-element>
    <x-element id="events" on-click="handleClick" on-tap="handleTap"></x-element>
    <template id="domIf" is="dom-if" if="[[prop]]">
      <x-element id="ifElement" prop="{{prop}}" path="{{obj.path}}"></x-element>
    </template>
    <!-- Nested template in Shadow DOM for runtime stamping -->
    <template id="templateFromShadowDom">
      <x-element early="[[earlyProp]]" id="first"></x-element>
      <x-element id="events" on-click="handleClick" on-tap="handleTap"></x-element>
      <template id="domIf" is="dom-if" if="[[prop]]">
        <x-element id="ifElementSD" prop="{{prop}}" path="{{obj.path}}"></x-element>
      </template>
      <span></span><span></span><span></span><span></span>
      <x-element id="compoundPropBinding" compound="[[prop]] - [[obj.path]] - [[compute(prop, obj.path)]]"></x-element>
      <x-element id="pathBinding" path="{{obj.path}}"></x-element>
      <x-element id="propBinding" prop="{{prop}}" log="shadow"></x-element>
      <x-element id="textBinding">[[prop]] - [[obj.path]] - [[compute(prop, obj.path)]]</x-element>
    </template>
  </template>
  <!-- Template in light DOM for runtime stamping -->
  <template id="templateFromLightDom">
    <x-element id="first"></x-element>
    <span></span><span></span><span></span><span></span>
    <x-element id="compoundPropBinding" compound="[[prop]] - [[obj.path]] - [[compute(prop, obj.path)]]"></x-element>
    <x-element id="pathBinding" path="{{obj.path}}"></x-element>
    <x-element id="propBinding" prop="{{prop}}" log="light"></x-element>
    <x-element id="textBinding">[[prop]] - [[obj.path]] - [[compute(prop, obj.path)]]</x-element>
    <x-element id="events" on-click="handleClick" on-tap="handleTap"></x-element>
    <template id="domIf" is="dom-if" if="[[prop]]">
      <x-element id="ifElementLD" prop="{{prop}}" path="{{obj.path}}"></x-element>
    </template>
  </template>
  <template id="templateWithDifferentProps">
    <div id="bound">[[otherProp]]</div>
  </template>
  <script type="module">
import { PolymerElement } from '../../polymer-element.js';
import { GestureEventListeners } from '../../lib/mixins/gesture-event-listeners.js';
import '../../lib/elements/dom-if.js';
import { DomModule } from '../../lib/elements/dom-module.js';
class XRuntime extends GestureEventListeners(PolymerElement) {
  static get is() { return 'x-runtime'; }
  static get observers() { return ['propChanged(prop)', 'pathChanged(obj.path)']; }
  constructor() {
    super();
    this.propChanged = sinon.spy();
    this.pathChanged = sinon.spy();
    this.prop = 'prop';
    this.obj = {path: 'obj.path'};
  }
  ready() {
    super.setAttribute('log', '');
    super.ready();
    window.lifecycleOrder.log(this, 'ready');
  }
  compute(a, b) {
    return `[${a} - ${b}]`;
  }
  stampTemplateFromShadow() {
    let dom = this._stampTemplate(this.$.templateFromShadowDom);
    this.shadowRoot.appendChild(dom);
    return dom;
  }
  stampTemplateAndSetPropFromShadow() {
    let dom = this._stampTemplate(this.$.templateFromShadowDom);
    this.earlyProp = 'early';
    this.shadowRoot.appendChild(dom);
    return dom;
  }
  stampTemplateFromLight() {
    let dom = this._stampTemplate(DomModule.import(this.localName, '#templateFromLightDom'));
    this.shadowRoot.appendChild(dom);
    return dom;
  }
  stampTemplateWithDifferentProps() {
    let dom = this._stampTemplate(DomModule.import(this.localName, '#templateWithDifferentProps'));
    this.shadowRoot.appendChild(dom);
    return dom;
  }
  stampNoAppend() {
    return this._stampTemplate(this.$.templateFromShadowDom);
  }
}
customElements.define('x-runtime', XRuntime);
</script>
</dom-module>

<script type="module">
  import { PolymerElement, html } from '../../polymer-element.js';
  customElements.define('x-runtime-nested', class extends PolymerElement {
    static get template() {
      return html`<template id="t1">
        <span>t1-[[prop1]]</span>
        <template id="t2">
          <span>t2-[[prop2]]</span>
          <template id="t3">
            <span>t3-[[prop3]]</span>
          </template>
        </template>
      </template>`;
    }
    static get properties() {
      return {
        prop1: { value: 'prop1'},
        prop2: { value: 'prop2'},
        prop3: { value: 'prop3'}
      };
    }
  });
</script>

<template id="custom-template">
  <x-special name="el1" special="attr1" binding="[[prop]]" on-event="handler"></x-special>
  <div name="el2" special="attr2">
    <div name="el3" special="attr3">
      <x-special name="el4" special="attr4"></x-special>
    </div>
    <div></div><div></div><div></div>
    <div name="el5" binding="[[prop]]" on-event="handler"></div>
    <template name="el6">
      <div>
        <x-special name="t-el" special="t-attr" binding="[[prop]]" on-event="handler"></x-special>
      </div>
    </template>
  </div>
  <x-special name="el7" special="attr5"><x-special name="el8" special="attr6"></x-special></x-special>
</template>
<script type="module">
import { PolymerElement } from '../../polymer-element.js';
import '../../lib/mixins/gesture-event-listeners.js';
import '../../lib/elements/dom-if.js';
class XParsing extends PolymerElement {
  static get template() { return document.getElementById('custom-template'); }
  static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value) {
    if (name == 'special') {
      nodeInfo.specialAttr = value;
      node.removeAttribute('special');
      node.setAttribute('had-special', '');
      return true;
    } else {
      return super._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value);
    }
  }
  static _parseTemplateNode(node, templateInfo, nodeInfo) {
    let noted = super._parseTemplateNode(node, templateInfo, nodeInfo);
    if (node.localName == 'x-special') {
      noted = nodeInfo.specialNode = true;
    }
    return noted;
  }
  _bindTemplate(template, instanceBinding) {
    return this.templateInfoForTesting = super._bindTemplate(template, instanceBinding);
  }
}
customElements.define('x-parsing', XParsing);

class XEffects extends XParsing {
  static get template() { return document.getElementById('custom-template'); }
  static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value) {
    let noted = super._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value);
    if (nodeInfo.specialAttr) {
      this._addTemplatePropertyEffect(templateInfo, 'attr', {
        fn(inst, property, props, oldProps, info, hasPaths, nodeList) {
          nodeList[nodeInfo.infoIndex].specialAttr = props[property];
        }
      });
    }
    return noted;
  }
  static _parseTemplateNode(node, templateInfo, nodeInfo) {
    let noted = super._parseTemplateNode(node, templateInfo, nodeInfo);
    if (nodeInfo.specialNode) {
      this._addTemplatePropertyEffect(templateInfo, 'node', {
        fn(inst, property, props, oldProps, info, hasPaths, nodeList) {
          nodeList[nodeInfo.infoIndex].specialNode = props[property];
        }
      });
    }
    return noted;
  }
}
customElements.define('x-effects', XEffects);
</script>

<dom-module id="x-binding">
  <template>
    <x-element id="standard1" prop="[[prop]]" path="[[obj.path]]"></x-element>
    <x-element id="custom1" prop='[{"a": "prop", "b": "prop2"}]'></x-element>
    <div>
      <x-element id="standard2" prop="[[prop]]" path="[[obj.path]]"></x-element>
      <x-element id="custom2" prop='[{"a": "prop", "b": "prop2"}]'></x-element>
    </div>
    <template id="domIf" is="dom-if" if="[[prop2]]" restamp>
      <x-element id="standard3" prop="[[prop]]" path="[[obj.path]]"></x-element>
      <x-element id="custom3" prop='[{"a": "prop", "b": "prop2"}]'></x-element>
    </template>
  </template>
  <script type="module">
import { PolymerElement } from '../../polymer-element.js';
import '../../lib/mixins/gesture-event-listeners.js';
import '../../lib/elements/dom-if.js';
class XBinding extends PolymerElement {
  static get is() { return 'x-binding'; }
  constructor() {
    super();
    this.prop = true;
    this.obj = {path: 'obj.path'};
    this.prop2 = true;
  }
  static _parseBindings(text, templateInfo) {
    if (text.slice(0,2) == '[{' && text.slice(-2) == '}]') {
      let bindingData = JSON.parse(text.slice(1,-1));
      let dependencies = Object.keys(bindingData).map(n=>bindingData[n]);
      return [{dependencies, bindingData}];
    } else {
      return super._parseBindings(text, templateInfo);
    }
  }
  static _evaluateBinding(scope, part, path, props, oldProps, hasPaths) {
    if (part.bindingData) {
      return Object.keys(part.bindingData)
        .map(n => scope[part.bindingData[n]] ? n : '')
        .filter(c => Boolean(c))
        .join(' ');
    } else {
      return super._evaluateBinding(scope, part, path, props, oldProps, hasPaths);
    }
  }
}
customElements.define(XBinding.is, XBinding);
</script>
</dom-module>

<script type="module">
import {PolymerElement, html} from '../../polymer-element.js';
import '../../lib/mixins/gesture-event-listeners.js';
import '../../lib/elements/dom-if.js';
import {setLegacyOptimizations} from '../../lib/utils/settings.js';

suite('runtime template stamping', function() {

  let el;

  setup(function() {
    window.lifecycleOrder = {
      log(el, lifecycle) {
        if (this.shouldLog(el)) {
          let list = this[lifecycle] = this[lifecycle] || [];
          list.push(this.idFor(el));
        }
      },
      shouldLog(el) {
        let host = el.getRootNode().host;
        return (!host || this.shouldLog(host)) && el.hasAttribute('log');
      },
      idFor(el) {
        let host = el.getRootNode().host;
        host = host && host.localName !== 'x-runtime' ? host : null;
        let id = el.getAttribute('log') || el.id;
        return (host ? this.idFor(host) + '|' : '') + el.localName + (id ? '#' + id : '');
      }
    };
    el = document.createElement('x-runtime');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  function assertStampingCorrect(el, $, type) {
    // Text binding
    assert.equal($.textBinding.textContent, 'prop - obj.path - [prop - obj.path]');
    // Property binding
    assert.equal($.propBinding.prop, 'prop');
    assert.equal($.propBinding.propChanged.callCount, 1);
    assert.equal($.propBinding.propChanged.firstCall.args[0], 'prop');
    // Path binding
    assert.equal($.pathBinding.path, 'obj.path');
    assert.equal($.pathBinding.pathChanged.callCount, 1);
    assert.equal($.pathBinding.pathChanged.firstCall.args[0], 'obj.path');
    // Compound property binding
    assert.equal($.compoundPropBinding.compound, 'prop - obj.path - [prop - obj.path]');
    // Observers
    assert.equal(el.propChanged.callCount, 1);
    assert.equal(el.propChanged.firstCall.args[0], 'prop');
    assert.equal(el.pathChanged.callCount, 1);
    assert.equal(el.pathChanged.firstCall.args[0], 'obj.path');
    // Event handlers
    el.handleClick = sinon.spy();
    el.handleTap = sinon.spy();
    $.events.click();
    assert.equal(el.handleClick.callCount, 1);
    assert.equal(el.handleTap.callCount, 1);
    // Nested dom-* template
    $.domIf.render();
    let ifElement = el.shadowRoot.querySelector(`#ifElement${type||''}`);
    assert.equal(ifElement.prop, 'prop');
    assert.equal(ifElement.path, 'obj.path');
    // Styling correct
    assert.equal(getComputedStyle($.textBinding).borderBottomWidth, '10px');
  }

  test('prototypical stamping', () => {
    assertStampingCorrect(el, el.$);
    let stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
    // Lifecycle order correct
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime'
    ]);
  });

  test('runtime stamp template (from shadow dom)', () => {
    let dom = el.stampTemplateFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom.$, 'SD');
    let stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom.$.first);
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow'
    ]);
  });

  test('runtime stamp and remove multiple templates (from shadow dom)', () => {
    let stamped;
    // Stamp template
    let dom1 = el.stampTemplateFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom1.$, 'SD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom1.$.first);
    // Unstamp
    el._removeBoundDom(dom1);
    for (let n in dom1.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
    // Stamp again
    let dom2 = el.stampTemplateFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom2.$, 'SD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom2.$.first);
    // Stamp again
    let dom3 = el.stampTemplateFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom2.$, 'SD');
    assertStampingCorrect(el, dom3.$, 'SD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 3);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom2.$.first);
    assert.equal(stamped[2], dom3.$.first);
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow'
    ]);
    // Unstamp
    el._removeBoundDom(dom2);
    el._removeBoundDom(dom3);
    for (let n in dom2.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    for (let n in dom3.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
  });

  test('runtime stamp template and set prop before attaching (from shadow dom)', () => {
    let dom = el.stampTemplateAndSetPropFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom.$, 'SD');
    let stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom.$.first);
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow'
    ]);
  });

  test('runtime stamp and remove multiple templates and set prop before attaching (from shadow dom)', () => {
    let stamped;
    // Stamp template
    let dom1 = el.stampTemplateAndSetPropFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom1.$, 'SD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom1.$.first);
    // Unstamp
    el._removeBoundDom(dom1);
    for (let n in dom1.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
    // Stamp again
    let dom2 = el.stampTemplateAndSetPropFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom2.$, 'SD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom2.$.first);
    // Stamp again
    let dom3 = el.stampTemplateAndSetPropFromShadow();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom2.$, 'SD');
    assertStampingCorrect(el, dom3.$, 'SD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 3);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom2.$.first);
    assert.equal(stamped[2], dom3.$.first);
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow',
      'x-element#shadow|x-element-child#noBinding',
      'x-element#shadow|x-element-child#hasBinding',
      'x-element#shadow'
    ]);
    // Unstamp
    el._removeBoundDom(dom2);
    el._removeBoundDom(dom3);
    for (let n in dom2.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    for (let n in dom3.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
  });

  test('runtime stamp template (from light dom)', () => {
    let dom = el.stampTemplateFromLight();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom.$, 'LD');
    let stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom.$.first);
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime',
      'x-element#light|x-element-child#noBinding',
      'x-element#light|x-element-child#hasBinding',
      'x-element#light'
    ]);
  });

  test('runtime stamp and remove multiple templates (from light dom)', () => {
    let stamped;
    // Stamp template
    let dom1 = el.stampTemplateFromLight();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom1.$, 'LD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom1.$.first);
    // Unstamp
    el._removeBoundDom(dom1);
    for (let n in dom1.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    // Stamp again
    let dom2 = el.stampTemplateFromLight();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom2.$, 'LD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 2);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom2.$.first);
    // Stamp again
    let dom3 = el.stampTemplateFromLight();
    assertStampingCorrect(el, el.$);
    assertStampingCorrect(el, dom2.$, 'LD');
    assertStampingCorrect(el, dom3.$, 'LD');
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 3);
    assert.equal(stamped[0], el.$.first);
    assert.equal(stamped[1], dom2.$.first);
    assert.equal(stamped[2], dom3.$.first);
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime',
      'x-element#light|x-element-child#noBinding',
      'x-element#light|x-element-child#hasBinding',
      'x-element#light',
      'x-element#light|x-element-child#noBinding',
      'x-element#light|x-element-child#hasBinding',
      'x-element#light',
      'x-element#light|x-element-child#noBinding',
      'x-element#light|x-element-child#hasBinding',
      'x-element#light'
    ]);
    // Unstamp
    el._removeBoundDom(dom2);
    el._removeBoundDom(dom3);
    for (let n in dom2.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    for (let n in dom3.$) {
      assert.notOk(dom1.$[n].parentNode, null);
    }
    stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
  });

  function assertPropValues(el, prop, value, count) {
    let e = el.$[prop + 'Binding'];
    assert.equal(e[prop], value);
    assert.equal(e[prop + 'Changed'].callCount, count);
    assert.equal(e[prop + 'Changed'].getCall(count-1).args[0], value);
    assert.equal(e.$.hasBinding[prop], value);
  }

  function assertAllPropValues(el, ld, sd, prop, value, count) {
    assertPropValues(el, prop, value, count);
    assertPropValues(ld, prop, value, count);
    assertPropValues(sd, prop, value, count);
  }

  test('downward runtime binding', () => {
    let sd = el.stampTemplateFromShadow();
    let ld = el.stampTemplateFromLight();
    assertAllPropValues(el, sd, ld, 'prop', 'prop', 1);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path', 1);
    el.prop = 'prop+';
    assertAllPropValues(el, sd, ld, 'prop', 'prop+', 2);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path', 1);
    el.obj = {path: 'obj.path+'};
    assertAllPropValues(el, sd, ld, 'prop', 'prop+', 2);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path+', 2);
    el.set('obj.path', 'obj.path++');
    assertAllPropValues(el, sd, ld, 'prop', 'prop+', 2);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path++', 3);
  });

  test('two-way runtime binding', () => {
    let sd = el.stampTemplateFromShadow();
    let ld = el.stampTemplateFromLight();
    assertAllPropValues(el, sd, ld, 'prop', 'prop', 1);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path', 1);
    el.$.propBinding.prop = 'prop+';
    assertAllPropValues(el, sd, ld, 'prop', 'prop+', 2);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path', 1);
    sd.$.propBinding.prop = 'prop++';
    assertAllPropValues(el, sd, ld, 'prop', 'prop++', 3);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path', 1);
    ld.$.propBinding.prop = 'prop+++';
    assertAllPropValues(el, sd, ld, 'prop', 'prop+++', 4);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path', 1);
    el.$.pathBinding.path = 'obj.path+';
    assertAllPropValues(el, sd, ld, 'prop', 'prop+++', 4);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path+', 2);
    sd.$.pathBinding.path = 'obj.path++';
    assertAllPropValues(el, sd, ld, 'prop', 'prop+++', 4);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path++', 3);
    ld.$.pathBinding.path = 'obj.path+++';
    assertAllPropValues(el, sd, ld, 'prop', 'prop+++', 4);
    assertAllPropValues(el, sd, ld, 'path', 'obj.path+++', 4);
  });

  test('accessors for non-prototypically bound properties created', () => {
    // First element
    let dom = el.stampTemplateWithDifferentProps();
    el.otherProp = 'otherProp';
    assert.equal(dom.$.bound.textContent, 'otherProp');
    // Second element
    let el2 = document.createElement('x-runtime');
    document.body.appendChild(el2);
    let dom2 = el2.stampTemplateWithDifferentProps();
    el2.otherProp = 'otherProp';
    assert.equal(dom2.$.bound.textContent, 'otherProp');
    document.body.removeChild(el2);
  });

  test('prototypical stamping not affected by runtime stamping', () => {
    assertStampingCorrect(el, el.$);
    let stamped = el.shadowRoot.querySelectorAll('x-element#first');
    assert.equal(stamped.length, 1);
    assert.equal(stamped[0], el.$.first);
    // Lifecycle order correct
    assert.deepEqual(window.lifecycleOrder.ready, [
      'x-element#proto|x-element-child#noBinding',
      'x-element#proto|x-element-child#hasBinding',
      'x-element#proto',
      'x-runtime'
    ]);
  });

  test('runtime stamped templates ready before append', () => {
    const dom = el.stampNoAppend();
    assert.isTrue(dom.querySelector('x-element').readied);
  });

});

suite('nested runtime template stamping', () => {

  let el;

  // Accumulates any spans that are stamped into a single ordered set; once
  // added, a span is never removed, so we can test that updates stop happening
  // when bound DOM is removed
  const spans = new Set();
  const accumulatedContent = () => {
    Array.from(el.shadowRoot.querySelectorAll('span')).forEach(s => spans.add(s));
    const content = [];
    spans.forEach(e => content.push(e.textContent));
    return content;
  };

  setup(() => {
    el = document.createElement('x-runtime-nested');
    document.body.appendChild(el);
  });
  teardown(() => {
    document.body.removeChild(el);
  });

  test('nested stamping', () => {
    // Stamp 1
    const dom1 = el._stampTemplate(el.shadowRoot.querySelector('#t1'));
    el.shadowRoot.appendChild(dom1);
    assert.deepEqual(accumulatedContent(), ['t1-prop1']);
    // Stamp 2
    const dom2 = el._stampTemplate(el.shadowRoot.querySelector('#t2'));
    el.shadowRoot.appendChild(dom2);
    assert.deepEqual(accumulatedContent(), ['t1-prop1', 't2-prop2']);
    // Stamp 3-1
    const dom3_1 = el._stampTemplate(el.shadowRoot.querySelector('#t3'));
    el.shadowRoot.appendChild(dom3_1);
    assert.deepEqual(accumulatedContent(), ['t1-prop1', 't2-prop2', 't3-prop3']);
    // Stamp 3_2
    const dom3_2 = el._stampTemplate(el.shadowRoot.querySelector('#t3'));
    el.shadowRoot.appendChild(dom3_2);
    assert.deepEqual(accumulatedContent(), ['t1-prop1', 't2-prop2', 't3-prop3', 't3-prop3']);
    // Stamp 3_3
    const dom3_3 = el._stampTemplate(el.shadowRoot.querySelector('#t3'));
    el.shadowRoot.appendChild(dom3_3);
    assert.deepEqual(accumulatedContent(), ['t1-prop1', 't2-prop2', 't3-prop3', 't3-prop3', 't3-prop3']);
    // Modify all
    el.setProperties({
      prop1: el.prop1 + '*',
      prop2: el.prop2 + '*',
      prop3: el.prop3 + '*'
    });
    assert.deepEqual(accumulatedContent(), ['t1-prop1*', 't2-prop2*', 't3-prop3*', 't3-prop3*', 't3-prop3*']);
    // Remove 3-1 & modify all
    el._removeBoundDom(dom3_1);
    el.setProperties({
      prop1: el.prop1 + '*',
      prop2: el.prop2 + '*',
      prop3: el.prop3 + '*'
    });
    assert.deepEqual(accumulatedContent(), ['t1-prop1**', 't2-prop2**', 't3-prop3*', 't3-prop3**', 't3-prop3**']);
    // Remove 3-3 & modify all
    el._removeBoundDom(dom3_3);
    el.setProperties({
      prop1: el.prop1 + '*',
      prop2: el.prop2 + '*',
      prop3: el.prop3 + '*'
    });
    assert.deepEqual(accumulatedContent(), ['t1-prop1***', 't2-prop2***', 't3-prop3*', 't3-prop3***', 't3-prop3**']);
    // Stamp 3-4
    const dom3_4 = el._stampTemplate(el.shadowRoot.querySelector('#t3'));
    el.shadowRoot.appendChild(dom3_4);
    assert.deepEqual(accumulatedContent(), ['t1-prop1***', 't2-prop2***', 't3-prop3*', 't3-prop3***', 't3-prop3**', 't3-prop3***']);
    // Modify all
    el.setProperties({
      prop1: el.prop1 + '*',
      prop2: el.prop2 + '*',
      prop3: el.prop3 + '*'
    });
    assert.deepEqual(accumulatedContent(), ['t1-prop1****', 't2-prop2****', 't3-prop3*', 't3-prop3****', 't3-prop3**', 't3-prop3****']);
  });
});

suite('template parsing hooks', () => {

  test('custom parsing', () => {
    let el = document.createElement('x-parsing');
    document.body.appendChild(el);
    let templateInfo = el.templateInfoForTesting;
    let nodeInfoList = templateInfo.nodeInfoList;
    let nodeList = templateInfo.nodeList;
    // The node order is depth-first bottom up but not a guarantee or generally
    // important; as such, just ensure all expected nodes are there, then
    // loop to
    assert.sameMembers(nodeList.map(e=>e.getAttribute('name')),
      ['el1', 'el2', 'el3', 'el4', 'el5', 'el6', 'el7', 'el8']);
    for (let i=0; i<nodeList.length; i++) {
      let node = nodeList[i];
      let nodeInfo = nodeInfoList[i];
      let templateNodeInfo;
      switch (node.getAttribute('name')) {
        case 'el1':
          assert.equal(nodeInfo.specialNode, true);
          assert.equal(nodeInfo.specialAttr, 'attr1');
          assert.equal(nodeInfo.bindings.length, 1);
          assert.equal(nodeInfo.events.length, 1);
          break;
        case 'el2':
          assert.equal(nodeInfo.specialAttr, 'attr2');
          break;
        case 'el3':
          assert.equal(nodeInfo.specialAttr, 'attr3');
          break;
        case 'el4':
          assert.equal(nodeInfo.specialNode, true);
          assert.equal(nodeInfo.specialAttr, 'attr4');
          break;
        case 'el5':
          assert.equal(nodeInfo.bindings.length, 1);
          assert.equal(nodeInfo.events.length, 1);
          break;
        case 'el6':
          assert.isOk(nodeInfo.templateInfo);
          assert.equal(nodeInfo.templateInfo.nodeInfoList.length, 1);
          templateNodeInfo = nodeInfo.templateInfo.nodeInfoList[0];
          assert.equal(templateNodeInfo.bindings.length, 1);
          assert.equal(templateNodeInfo.events.length, 1);
          assert.equal(templateNodeInfo.specialAttr, 't-attr');
          assert.equal(templateNodeInfo.specialNode, true);
          break;
        case 'el7':
          assert.equal(nodeInfo.specialNode, true);
          assert.equal(nodeInfo.specialAttr, 'attr5');
          break;
        case 'el8':
          assert.equal(nodeInfo.specialNode, true);
          assert.equal(nodeInfo.specialAttr, 'attr6');
          break;
        default:
          throw new Error('unexpected node was recorded');
      }
    }
  });

  test('custom template effects', () => {
    let el = document.createElement('x-effects');
    document.body.appendChild(el);

    assert.equal(Array.from(el.shadowRoot.querySelectorAll('x-special')).length, 4);
    Array.from(el.shadowRoot.querySelectorAll('x-special')).forEach(e => {
      assert.notOk(e.isSpecialNode);
    });
    el.node = 'node!';
    Array.from(el.shadowRoot.querySelectorAll('x-special')).forEach(e => {
      assert.equal(e.specialNode, 'node!');
    });

    assert.equal(Array.from(el.shadowRoot.querySelectorAll('[had-special]')).length, 6);
    Array.from(el.shadowRoot.querySelectorAll('[had-special]')).forEach(e => {
      assert.notOk(e.hasSpecialAttr);
    });
    el.attr = 'attr!';
    Array.from(el.shadowRoot.querySelectorAll('[had-special]')).forEach(e => {
      assert.equal(e.specialAttr, 'attr!');
    });
    document.body.removeChild(el);
  });

  test('custom template binding', () => {
    let el = document.createElement('x-binding');
    document.body.appendChild(el);
    el.$.domIf.render();
    assert.equal(el.$.standard1.prop, true);
    assert.equal(el.$.standard2.prop, true);
    assert.equal(el.shadowRoot.querySelector('#standard3').prop, true);
    assert.equal(el.$.standard1.path, 'obj.path');
    assert.equal(el.$.standard2.path, 'obj.path');
    assert.equal(el.shadowRoot.querySelector('#standard3').path, 'obj.path');
    assert.equal(el.$.custom1.prop, 'a b');
    assert.equal(el.$.custom2.prop, 'a b');
    assert.equal(el.shadowRoot.querySelector('#custom3').prop, 'a b');
    el.prop = false;
    assert.equal(el.$.standard1.prop, false);
    assert.equal(el.$.standard2.prop, false);
    assert.equal(el.shadowRoot.querySelector('#standard3').prop, false);
    assert.equal(el.$.standard1.path, 'obj.path');
    assert.equal(el.$.standard2.path, 'obj.path');
    assert.equal(el.shadowRoot.querySelector('#standard3').path, 'obj.path');
    assert.equal(el.$.custom1.prop, 'b');
    assert.equal(el.$.custom2.prop, 'b');
    assert.equal(el.shadowRoot.querySelector('#custom3').prop, 'b');
    el.prop = true;
    assert.equal(el.$.standard1.prop, true);
    assert.equal(el.$.standard2.prop, true);
    assert.equal(el.shadowRoot.querySelector('#standard3').prop, true);
    assert.equal(el.$.standard1.path, 'obj.path');
    assert.equal(el.$.standard2.path, 'obj.path');
    assert.equal(el.shadowRoot.querySelector('#standard3').path, 'obj.path');
    assert.equal(el.$.custom1.prop, 'a b');
    assert.equal(el.$.custom2.prop, 'a b');
    assert.equal(el.shadowRoot.querySelector('#custom3').prop, 'a b');
    document.body.removeChild(el);
  });
});

suite('textarea placeholder bug', function() {
  class PlaceholderBase extends PolymerElement {
    static get template() {
      return html`<textarea id="textarea" placeholder="[[value]]"></textarea>`;
    }
    static get properties() {
      return {value: {type: String}};
    }
  }
  test('placeholder binding does not leak to textContent', function() {
    customElements.define('placeholder-duplicate', class extends PlaceholderBase {});
    const el = document.createElement('placeholder-duplicate');
    document.body.appendChild(el);
    const textarea = el.$.textarea;
    el.value = 'before';
    textarea.value = 'Hello!';
    el.value = 'after';
    assert.equal(textarea.value, 'Hello!');
  });
  suite('legacyOptimizations', function() {
    suiteSetup(function() {
      setLegacyOptimizations(true);
    });
    suiteTeardown(function() {
      setLegacyOptimizations(false);
    });
    test('textarea placeholder binding works with legacyOptimizations', function() {
      customElements.define('placeholder-bug', class extends PlaceholderBase {});
      const el = document.createElement('placeholder-bug');
      document.body.appendChild(el);
      assert.doesNotThrow(() => {el.value = 'bar';});
    });
  });
});
</script>

</body>
</html>
